package mspbots.hubspot.client.oauth;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;
import mspbots.hubspot.client.annotation.ApiException;
import mspbots.hubspot.client.core.client.ClientProperties;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.expression.ExpressionException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.time.Duration;

import static mspbots.hubspot.common.RedisKeys.ACCESS_TOKEN_REDIS_KEY;
import static mspbots.hubspot.common.RedisKeys.AUTHORIZE_REDIS_KEY;

/**
 * com.mspbots.doctor.core.AuthService
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2020/3/19
 */
@Log4j2
@Service
public class AuthService {

    private final WebClient webClient;
    private final ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
    private final ObjectMapper objectMapper;
    private final ClientProperties doctorProperties;

    public AuthService(WebClient.Builder builder, ReactiveRedisTemplate<String, Object> reactiveRedisTemplate,
                       ObjectMapper objectMapper, ClientProperties doctorProperties) {
        this.webClient = builder.baseUrl("https://api.hubapi.com/")
                .defaultHeader("Accept", "application/json").build();
        this.reactiveRedisTemplate = reactiveRedisTemplate;
        this.objectMapper = objectMapper;
        this.doctorProperties = doctorProperties;
    }

    public Mono<Authorization> authToken(Long tenantId, String code) {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.set("grant_type", "authorization_code");
        params.set("redirect_uri", this.doctorProperties.getRedirectUri());
        params.set("code", code);
        return this.tokenClientGet(tenantId, params);
    }

    public Mono<Authorization> getToken(Long tenantId) {

        Mono<String> accessTokenMono = this.reactiveRedisTemplate.opsForValue()
                .get(ACCESS_TOKEN_REDIS_KEY + tenantId)
                .map(result -> this.objectMapper.convertValue(result, String.class));

        Mono<Authorization> authorizationCodeMono = this.reactiveRedisTemplate.opsForValue()
                .get(AUTHORIZE_REDIS_KEY + tenantId)
                .switchIfEmpty(Mono.defer(() ->
                        Mono.error(new ExpressionException("Your access token is empty,Allow access your account."))))
                .map(result -> this.objectMapper.convertValue(result, Authorization.class));

        return accessTokenMono
                .switchIfEmpty(authorizationCodeMono
                        .flatMap(authorization -> this.refreshToken(tenantId, authorization.getRefreshToken()))
                        .map(Authorization::getAccessToken))
                .map(result -> Authorization.builder().tenantId(tenantId).accessToken(result).build());

    }

    private Mono<Authorization> refreshToken(Long tenantId, String refreshToken) {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.set("grant_type", "refresh_token");
        params.set("refresh_token", refreshToken);
        return this.tokenClientGet(tenantId, params);
    }


    private Mono<Authorization> tokenClientGet(Long tenantId, MultiValueMap<String, String> params) {
        params.set("client_id", this.doctorProperties.getClientId());
        params.set("client_secret", this.doctorProperties.getSecretKey());
        log.debug("Request post Authorization params {}", params);
        return this.webClient.post().uri(uriBuilder -> uriBuilder.path("/oauth/v1/token").build())
                .bodyValue(params)
                .retrieve()
                .onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(JsonNode.class)
                        .map(ApiException::withMsg))
                .bodyToMono(JsonNode.class)
                .onErrorResume(err -> {
                    log.error("Authorization token client get error, msg: {}", err.getMessage());
                    return Mono.error(err);
                })
                .map(jsonNode -> Authorization.builder()
                        .tenantId(tenantId)
                        .accessToken(jsonNode.get("access_token").textValue())
                        .refreshToken(jsonNode.get("refresh_token").textValue())
                        .expiresIn(jsonNode.get("expires_in").longValue())
                        .build())
                .doOnNext(authCode -> this.reactiveRedisTemplate.opsForValue()
                        .set(AUTHORIZE_REDIS_KEY + tenantId, authCode,
                                Duration.ofDays(36500)).subscribe())
                .doOnNext(authCode -> this.reactiveRedisTemplate.opsForValue()
                        .set(ACCESS_TOKEN_REDIS_KEY + tenantId, authCode.getAccessToken(),
                                Duration.ofSeconds(authCode.getExpiresIn())).subscribe());
    }

}
